/** * Copyright (C) 2011 BonitaSoft S.A. * BonitaSoft, 32 rue Gustave Eiffel - 38000 Grenoble * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2.0 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.bonitasoft.web.toolkit.client.ui.component.form; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Stack; import org.bonitasoft.web.toolkit.client.common.AbstractTreeNode; import org.bonitasoft.web.toolkit.client.common.Tree; import org.bonitasoft.web.toolkit.client.common.TreeIndexed; import org.bonitasoft.web.toolkit.client.common.TreeLeaf; import org.bonitasoft.web.toolkit.client.common.TreeNode; import org.bonitasoft.web.toolkit.client.common.i18n.AbstractI18n; import org.bonitasoft.web.toolkit.client.common.json.JSonSerializer; import org.bonitasoft.web.toolkit.client.common.json.JSonUtil; import org.bonitasoft.web.toolkit.client.common.json.JsonSerializable; import org.bonitasoft.web.toolkit.client.common.texttemplate.Arg; import org.bonitasoft.web.toolkit.client.common.texttemplate.TextTemplate; import org.bonitasoft.web.toolkit.client.data.item.attribute.validator.Validator; import org.bonitasoft.web.toolkit.client.ui.JsId; import org.bonitasoft.web.toolkit.client.ui.action.Action; import org.bonitasoft.web.toolkit.client.ui.component.containers.Container; import org.bonitasoft.web.toolkit.client.ui.component.core.Component; import org.bonitasoft.web.toolkit.client.ui.component.event.InputCompleteEvent; import org.bonitasoft.web.toolkit.client.ui.component.event.InputCompleteHandler; import org.bonitasoft.web.toolkit.client.ui.component.form.button.FormButton; import org.bonitasoft.web.toolkit.client.ui.component.form.entry.AutoCompleteEntry; import org.bonitasoft.web.toolkit.client.ui.component.form.entry.FormEntries; import org.bonitasoft.web.toolkit.client.ui.component.form.entry.FormEntry; import org.bonitasoft.web.toolkit.client.ui.component.form.entry.Section; import org.bonitasoft.web.toolkit.client.ui.component.form.entry.StaticText; import org.bonitasoft.web.toolkit.client.ui.component.form.entry.ValuedFormEntry; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONNull; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONValue; import com.google.gwt.query.client.Function; import com.google.gwt.query.client.GQuery; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; /** * @author Séverin Moussel */ public abstract class AbstractForm extends Component implements JsonSerializable { private final TreeIndexed<String> hiddens = new TreeIndexed<String>(); private HashMap<String, ValuedFormEntry> entriesIndex = new HashMap<String, ValuedFormEntry>(); /** * A stack of opened container with at least the root container of FormEntries in it. */ protected Stack<Container<FormNode>> containers = new Stack<Container<FormNode>>(); /** * The container of FormActions. */ private Container<FormButton> buttons = new Container<FormButton>(new JsId("formactions")); // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CONSTRUCTORS // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public AbstractForm() { super(null); resetEntries(); } public AbstractForm(final JsId jsid) { super(jsid); resetEntries(); } /** * Default Constructor. * * @param jsid */ public AbstractForm(final JsId jsid, final HashMap<String, String> hiddens) { super(jsid); this.hiddens.addValues(hiddens); resetEntries(); } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ENTRIES // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// protected final void addHidden(final String name, final String value) { AbstractTreeNode<String> node = hiddens.get(name); if (node == null) { hiddens.addNode(name, new TreeLeaf<String>(value)); } else if (node instanceof TreeLeaf<?>) { final String oldValue = ((TreeLeaf<String>) node).getValue(); if (!oldValue.equals(value)) { hiddens.removeNode(name); node = new Tree<String>(); hiddens.addNode(name, node); ((Tree<String>) node).addValue(oldValue); ((Tree<String>) node).addValue(value); } } else if (node instanceof Tree<?>) { ((Tree<String>) hiddens.get(name)).addValue(value); } } protected final void addHidden(final String name, final List<String> values) { hiddens.addNode(name, new Tree<String>().addValues(values)); } protected final void addHidden(final String name, final TreeNode<String> values) { hiddens.addNode(name, values); } /** * Get the value of an entry as an array using its JsId * * @param jsid * @return This function returns the current value filled in the entry as a String. */ public List<String> getEntryArrayValue(final JsId jsid) { final List<String> results = new ArrayList<String>(); // Search in hidden fields final AbstractTreeNode<String> hiddenValue = hiddens.get(jsid.toString()); if (hiddenValue != null) { if (hiddenValue instanceof Tree<?>) { results.addAll(((Tree<String>) hiddenValue).getValues()); } else if (hiddenValue instanceof TreeLeaf<?>) { results.add(((TreeLeaf<String>) hiddenValue).getValue()); } } // Search in input fields if (getEntry(jsid) != null) { results.addAll(getFormArrayParameter(getElement(), jsid.toString())); } return results; } /** * Get the value of an entry using its JsId * * @param jsid * @return This function returns the current value filled in the entry as a String. */ public String getEntryValue(final JsId jsid) { // Search in hidden fields final AbstractTreeNode<String> result = hiddens.get(jsid.toString()); if (result != null) { if (result instanceof Tree<?>) { if (((Tree<String>) result).size() != 1) { return null; } return ((Tree<String>) result).getValues().get(0); } else if (result instanceof TreeLeaf<?>) { return ((TreeLeaf<String>) result).getValue(); } else { return null; } } // Search in input fields else { final ValuedFormEntry entry = getEntry(jsid); if (entry != null) { return entry.getValue(); } } return null; } public List<String> getEntryArrayValue(final String name) { return this.getEntryArrayValue(new JsId(name)); } public String getEntryValue(final String name) { return this.getEntryValue(new JsId(name)); } public void setEntryValue(final String name, final String value) { if (entriesIndex.get(name) != null) { entriesIndex.get(name).setValue(value); } else if (hiddens.containsKey(name)) { hiddens.removeNode(name); this.addHidden(name, value); } } public AbstractForm resetEntries() { entriesIndex = new HashMap<String, ValuedFormEntry>(); containers = new Stack<Container<FormNode>>(); buttons = new Container<FormButton>(new JsId("formactions")); openSection(new FormEntries()); return this; } /** * Empty all inputs * * @return This method returns "this" to allow cascading calls. */ public AbstractForm reset() { resetErrors(); reset(getElement()); return this; } private native void reset(Element form) /*-{ $wnd.$("input:text, textarea", form).val(""); // $wnd.$("select option", getElement()).removeAttr("selected") // .first().attr("selected", "selected"); $wnd.$("input:checkbox, input:radio", form).removeAttr("checked"); }-*/; /** * @return the entriesIndex */ public HashMap<String, ValuedFormEntry> getEntriesIndex() { return entriesIndex; } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // SECTIONS // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Open a new section and make it the target of the next <i>addEntry</i> and <i>addSection</i> until <i>closeLastSection</i> is called. * * @param section * The section to add */ public final AbstractForm openSection(final Section section) { // A section can't contain a section as direct child if (containers.size() > 1 && containers.lastElement() instanceof FormEntries) { closeSection(); } if (containers.size() > 0) { getLastContainer().append(section); } containers.push(section); addValuableEntry(section); return this; } private void addValuableEntries(final LinkedList<FormNode> nodes) { for (final FormNode node : nodes) { addValuableEntry(node); } } private void addValuableEntry(final FormNode node) { if (isValuable(node)) { entriesIndex.put(node.getJsId().toString(), (ValuedFormEntry) node); } if (isContainer(node)) { addValuableEntries(((Section) node).getComponents()); } } private boolean isValuable(final FormNode node) { return node instanceof ValuedFormEntry; } private boolean isContainer(final FormNode node) { return node instanceof Section; } /** * Close the currently opened Section.<br> * If no section stacked, the function does nothing */ public final AbstractForm closeSection() { containers.pop(); return this; } /** * Retrieve the last opened entries container */ protected Container<FormNode> getLastContainer() { // If no formEntries container exists, open a new one if (containers.size() == 0) { containers.push(new Section()); } return containers.lastElement(); } @SuppressWarnings("unchecked") protected Container<FormNode> getContainerFor(final JsId jsid) { return (Container<FormNode>) getEntry(jsid).getContainer(); } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ENTRIES // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void addEntry(final FormNode entry) { if (entry instanceof Section) { addEntry((Section) entry); } else if (entry instanceof FormEntry) { addEntry((FormEntry) entry); } else { if (entry instanceof Component) { ((Component) entry).addClass("formentry"); } getLastContainer().append(entry); } } /** * Section is the highest container implementing FormNode * * @param section */ private void addEntry(final Section section) { openSection(section); } /** * Add an entry to the form in the last opened section if it exists * * @param entry */ private void addEntry(final FormEntry entry) { if (entry instanceof AutoCompleteEntry) { ((AutoCompleteEntry) entry).addInputHandler(createInputCompleteHandler(), InputCompleteEvent.TYPE); } getLastContainer().append(entry); entriesIndex.put(entry.getJsId().toString(), entry); } private InputCompleteHandler createInputCompleteHandler() { return new InputCompleteHandler() { @Override public void onComplete(final InputCompleteEvent event) { AbstractForm.this.resetErrors(); } }; } /** * Add an entry to the form in the last opened section if it exists * * @param entry */ protected final void addEntryBefore(final FormEntry entry, final JsId jsid) { getLastContainer().prepend(entry); entriesIndex.put(entry.getJsId().toString(), entry); } /** * Add an entry to the form in the last opened section if it exists * * @param entry */ protected final void addEntryAfter(final FormEntry entry, final JsId jsid) { getLastContainer().append(entry); entriesIndex.put(entry.getJsId().toString(), entry); } /** * Get an entry by its jsid * * @param jsid */ public ValuedFormEntry getEntry(final JsId jsid) { return entriesIndex.get(jsid.toString()); } /** * JQuery reading of an input value by its name * * @param form * @param name * @return */ private native String getFormParameter(final Element form, final String name) /*-{ var input = $wnd.$(form).find('[name=' + name + ']'); if (input.is(':checkbox') || input.is(':radio')) { input = $wnd.$(form).find('[name=' + name + ']:checked'); } return input.val(); }-*/; public boolean isChecked(final JsId jsid) { return this.isChecked(jsid.toString()); } public boolean isChecked(final String jsid) { return _isChecked(element, jsid); } private native boolean _isChecked(final Element form, final String name) /*-{ return $wnd.$(form).find('[name=' + name + ']').checked(); }-*/; private List<String> getFormArrayParameter(final Element form, final String name) { final List<String> result = new ArrayList<String>(); GQuery.$(form).find("[name=" + name + "]").each(new Function() { @Override public void f(final Element e) { final String value = AbstractForm.this._getFormInputParameter(e); if (value != null) { result.add(value); } } }); return result; } private native String _getFormInputParameter(final Element element) /*-{ var input = $wnd.$(element); if ((input.is(':checkbox') || input.is(':radio')) && !input.is(':checked')) { return null; } return $wnd.$(element).val(); }-*/; private native void setFormParameter(Element form, String name, String value) /*-{ var input = $wnd.$(form).find('[name=' + name + ']'); if (input.is(':checkbox')) { input.check(input.val() == value); } else if (input.is(':radio')) { input .uncheck() .filter('[value=' + value + ']').check(); } else { input.val(value); } }-*/; // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ACTIONS // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// protected Action defaultAction = null; /** * @return the defaultAction */ protected final Action getDefaultAction() { return defaultAction; } /** * @param defaultAction * the defaultAction to set */ protected final void setDefaultAction(final Action defaultAction) { this.defaultAction = defaultAction; } /** * Add an action to the action container * * @param button */ protected void addAction(final FormButton button) { if (defaultAction == null) { defaultAction = button.getAction(); } buttons.append(button); } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // DOM GENERATION // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Override protected Element makeElement() { final Element form = DOM.createForm(); form.addClassName("form"); if (getJsId() != null) { form.addClassName(getJsId().toString("form")); } form.appendChild(containers.firstElement().getElement()); form.appendChild(buttons.getElement()); GQuery.$(form).submit(new Function() { @Override public boolean f(final Event e) { e.stopPropagation(); defaultAction.execute(); return false; } }); addFocusEventForIE(form); return form; } private native void addFocusEventForIE(Element form) /*-{ try { $wnd.addFocusEventForIE(form); } catch(e) { //do nothing } }-*/; @Override public String toJson() { return JSonSerializer.serialize(getValues()); } public TreeIndexed<String> getValues() { final TreeIndexed<String> allParameters = hiddens.copy(); // Get the entries values for (final Entry<String, ValuedFormEntry> entry : entriesIndex.entrySet()) { if (entry.getValue() != null) { allParameters.addValue(entry.getKey(), getEntryValue(entry.getKey())); } } return allParameters; } public void setValues(final Map<String, String> values) { for (final Entry<String, String> entry : values.entrySet()) { setEntryValue(entry.getKey(), entry.getValue()); } } public void setJson(final String json) { // TODO Improve filling multilevel arrays final JSONValue parsedJson = JSONParser.parseStrict(json); if (parsedJson.isArray() != null) { final JSONArray jsonArray = parsedJson.isArray(); for (int i = 0; i < jsonArray.size(); i++) { setJson((JSONObject) jsonArray.get(i)); } } else if (parsedJson.isObject() != null) { setJson(parsedJson.isObject()); } else { throw new IllegalArgumentException("Malformed JSON"); } } public void setJson(final JSONObject item) { final Iterator<String> j = item.keySet().iterator(); while (j.hasNext()) { final String key = j.next(); if (item.get(key) instanceof JSONObject) { setJson(composeArrayKey(key, (JSONObject) item.get(key))); } else { String value; if (item.get(key) instanceof JSONNull) { value = null; } else { value = JSonUtil.unquote(item.get(key).isString().toString()); } setEntryValue(key, value); } } } private JSONObject composeArrayKey(final String prefix, final JSONObject item) { final JSONObject composedArray = new JSONObject(); final Iterator<String> j = item.keySet().iterator(); while (j.hasNext()) { final String key = j.next(); composedArray.put(prefix + "_" + key, item.get(key)); } return composedArray; } private void setJson(final JSONValue json) { if (json.isArray() != null) { final int size = json.isArray().size(); for (int i = 0; i < size; i++) { this.setJson(json.isArray().get(i)); } } else if (json.isObject() != null) { final JSONObject item = json.isObject(); for (final String key : item.keySet()) { String value; if (item.get(key) instanceof JSONNull) { continue; } else { value = item.get(key).isString().stringValue(); } setEntryValue(key, value); } } } public boolean hasNonStaticEntry() { for (final ValuedFormEntry entry : entriesIndex.values()) { if (!(entry instanceof StaticText)) { return true; } } return false; } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ERROR MESSAGES // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// public void addError(final JsId jsid, final String message) { getEntry(jsid).addError(message); } public void addError(final JsId jsid, final TextTemplate messageTemplate) { final ValuedFormEntry entry = getEntry(jsid); final List<Arg> args = new ArrayList<Arg>(); // Fill args with the label of entries corresponding to parameters in the message; for (final String parameterName : messageTemplate.getExpectedParameters()) { if (entry instanceof FormEntry) { if (entry.getJsId().toString().equals(parameterName)) { args.add(new Arg(parameterName, AbstractI18n._("this field"))); } else { args.add(new Arg(parameterName, ((FormEntry) entry).getLabel())); } } else { args.add(new Arg(parameterName, parameterName)); } } final String messageOutput = messageTemplate.toString(args); entry.addError(messageOutput.substring(0, 1).toUpperCase() + messageOutput.substring(1)); } public void resetErrors() { GQuery.$("div.alert_message", getElement()).remove(); } public Map<String, List<Validator>> getValidators() { final Map<String, List<Validator>> validators = new HashMap<String, List<Validator>>(); for (final java.util.Map.Entry<String, ValuedFormEntry> entry : entriesIndex.entrySet()) { validators.put(entry.getKey(), entry.getValue().getValidators()); } return validators; } // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // VALIDATORS AND MODIFIERS // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * @Deprecated * Use {@link ValuedFormEntry#addValidator(Validator)} */ @Deprecated public AbstractForm addValidator(final JsId entryJsId, final Validator validator) { getEntry(entryJsId).addValidator(validator); return this; } /** * @Deprecated * Use {@link ValuedFormEntry#addValidator(Validator)} */ @Deprecated public AbstractForm addValidator(final String entryJsId, final Validator validator) { return addValidator(new JsId(entryJsId), validator); } }